﻿using System;
using System.Collections.Generic;
using System.Threading;

using FireBus.Contracts;
using FireBus.Core.Configuration;


namespace FireBus.Core
{
	public class Host : IDisposable
	{
		public event EventHandler<ExtensionEventArgs> ExtensionLoaded;

		public event EventHandler<ExtensionEventArgs> ExtensionUnloaded;
		

		private readonly ReaderWriterLockSlim _Sync = new ReaderWriterLockSlim(LockRecursionPolicy.NoRecursion);

		private readonly IDictionary<ExtensionName, ExtensionToken> _Tokens
			= new Dictionary<ExtensionName, ExtensionToken>();

		private readonly IDictionary<ExtensionName, Extension> _Extensions
			= new Dictionary<ExtensionName, Extension>();

		private readonly IExtensionLoader _Loader;

		private readonly IMessageAccessor _MessageAccessor;

		private volatile Boolean _Disposed;

		private readonly IStorage _Storage;


		public Host(IConfiguration configuration)
		{
			if (configuration == null)
			{
				throw new ArgumentNullException("configuration");
			}
			var messageAccessor = configuration.GetMessageAccessor();
			var extensionAccessor = new ExtensionAccessor(this);
			_Loader = new IsolatedExtensionLoader(messageAccessor, extensionAccessor);
			_MessageAccessor = messageAccessor;
			var tokens = configuration.GetExtensionTokens();
			foreach (var token in tokens)
			{
				AddExtension(token);
			}
			_Storage = new Storage(this);
		}


		public void AddExtension(ExtensionToken token)
		{
			if (token == null)
			{
				throw new ArgumentNullException("token");
			}
			_Sync.EnterWriteLock();
			try
			{
				if (_Tokens.ContainsKey(token.Name))
				{
					throw new CannotAddExtensionException(
						_MessageAccessor.ExtensionWithGivenNameAlreadyExists(token.Name));
				}
				_Tokens.Add(token.Name, token);
			}
			finally
			{
				_Sync.ExitWriteLock();
			}
		}

		public void RemoveExtension(ExtensionName name)
		{
			_Sync.EnterWriteLock();
			try
			{
				RemoveExtensionWithNoLocks(name);
			}
			finally
			{
				_Sync.ExitWriteLock();
			}
		}

		private void RemoveExtensionWithNoLocks(ExtensionName name)
		{
			if (!_Tokens.ContainsKey(name))
			{
				return;
			}
			UnloadExtensionWithNoLocks(name);
			_Tokens.Remove(name);
		}

		private void RemoveAllExtensionsWithNoLocks()
		{
			var names = new List<ExtensionName>(_Tokens.Keys);
			foreach (var name in names)
			{
				RemoveExtensionWithNoLocks(name);
			}
		}

		public IEnumerable<ExtensionToken> GetExtensions()
		{
			_Sync.EnterReadLock();
			try
			{
				var tokens = new List<ExtensionToken>(_Tokens.Values);
				return tokens;
			}
			finally
			{
				_Sync.ExitReadLock();
			}
		}

		public ExtensionState GetExtensionState(ExtensionName name)
		{
			_Sync.EnterReadLock();
			try
			{
				return GetExtensionStateWithNoLocks(name);
			}
			finally
			{
				_Sync.ExitReadLock();
			}
		}

		private ExtensionState GetExtensionStateWithNoLocks(ExtensionName name)
		{
			if (!_Tokens.ContainsKey(name))
			{
				return ExtensionState.NotAvailable;
			}
			if (!_Extensions.ContainsKey(name))
			{
				return ExtensionState.Stopped;
			}
			return ExtensionState.Started;
		}

		public IAsyncResult BeginLoadAllExtensions(AsyncCallback callback, Object state)
		{
			var asyncResult = new VoidAsyncResult(callback, state, _MessageAccessor);
			ThreadPool.QueueUserWorkItem(
				delegate
				{
					try
					{
						LoadAllExtensions();
						asyncResult.SetAsCompleted(null, false);
					}
					catch (Exception e)
					{
						asyncResult.SetAsCompleted(e, false);
					}
				});
			return asyncResult;
		}

		private void LoadAllExtensions()
		{
			_Sync.EnterWriteLock();
			try
			{
				var tokens = new List<ExtensionToken>();
				foreach (var pair in _Tokens)
				{
					var token = pair.Value;
					if (GetExtensionStateWithNoLocks(token.Name) == ExtensionState.Stopped)
					{
						tokens.Add(token);
					}
				}
				var waitHandles = new WaitHandle[tokens.Count];
				
				for(var i=0; i<tokens.Count; i++)
				{
					var token = tokens[i];
					var asyncResult = new VoidAsyncResult(null, null, _MessageAccessor);
					ThreadPool.QueueUserWorkItem(
						delegate
						{
							try
							{
								LoadExtensionWithNoLocks(token.Name);
								asyncResult.SetAsCompleted(null, false);
							}
							catch (Exception e)
							{
								asyncResult.SetAsCompleted(e, false);
							}
						});
					waitHandles[i] = asyncResult.AsyncWaitHandle;
				}
				if (waitHandles.Length > 0)
				{
					WaitHandle.WaitAll(waitHandles);
				}
			}
			finally
			{
				_Sync.ExitWriteLock();
			}
			BeginNotifyAll(
				new ExtensionStorageInitialize(_Storage), null, null);
		}

		public void EndLoadAllExtensions(IAsyncResult asyncResult)
		{
			var voidAsyncResult = (VoidAsyncResult)asyncResult;
			voidAsyncResult.EndInvoke();
		}

		public IAsyncResult BeginLoadExtension(ExtensionName extensionName, AsyncCallback callback, Object state)
		{
			if (extensionName == null)
			{
				throw new ArgumentNullException("extensionName");
			}
			var asyncResult = new VoidAsyncResult(callback, state, _MessageAccessor);
			ThreadPool.QueueUserWorkItem(
				delegate
				{
					try
					{
						LoadExtension(extensionName);
						asyncResult.SetAsCompleted(null, false);
					}
					catch (Exception e)
					{
						asyncResult.SetAsCompleted(e, false);
					}
				});
			return asyncResult;
		}

		private void LoadExtension(ExtensionName name)
		{
			_Sync.EnterWriteLock();
			try
			{
				LoadExtensionWithNoLocks(name);
			}
			finally
			{
				_Sync.ExitWriteLock();
			}
			var metadata = GetExtensionMetadata(name);
			if (!metadata.CanListenToEvent(typeof(ExtensionStorageInitialize).AssemblyQualifiedName))
			{
				return;
			}
			BeginNotify(name, new ExtensionStorageInitialize(_Storage), null, null);
		}

		private ExtensionMetadata GetExtensionMetadata(ExtensionName name)
		{
			_Sync.EnterReadLock();
			try
			{
				Extension extension;
				if (!_Extensions.TryGetValue(name, out extension))
				{
					return null;
				}
				return extension.Metadata;
			}
			finally
			{
				_Sync.ExitReadLock();
			}
		}

		private void LoadExtensionWithNoLocks(ExtensionName name)
		{
			ExtensionToken token;
			if (!_Tokens.TryGetValue(name, out token))
			{
				return;
			}
			if (_Extensions.ContainsKey(name))
			{
				return;
			}
			var loaderResult = _Loader.Load(token);
			if (loaderResult.Failed)
			{
				return;
			}
			var extension = loaderResult.Extension;
			_Extensions.Add(name, extension);
			OnExtensionLoaded(name, extension.Metadata);
		}

		virtual protected void OnExtensionLoaded(ExtensionName extension, ExtensionMetadata metadata)
		{
			if (ExtensionLoaded != null)
			{
				ThreadPool.QueueUserWorkItem(
					delegate
						{
							ExtensionLoaded(
								this, new ExtensionEventArgs(extension, metadata));
						});
			}
		}

		public void EndLoadExtension(IAsyncResult asyncResult)
		{
			var voidAsyncResult = (VoidAsyncResult)asyncResult;
			voidAsyncResult.EndInvoke();
		}

		public IAsyncResult BeginUnloadExtension(ExtensionName extensionName, AsyncCallback callback, Object state)
		{
			var asyncResult = new VoidAsyncResult(callback, state, _MessageAccessor);
			ThreadPool.QueueUserWorkItem(
				delegate
				{
					try
					{
						UnloadExtension(extensionName);
						asyncResult.SetAsCompleted(null, false);
					}
					catch (Exception e)
					{
						asyncResult.SetAsCompleted(e, false);
					}
				});
			return asyncResult;
		}

		private void UnloadExtension(ExtensionName name)
		{
			_Sync.EnterWriteLock();
			try
			{
				UnloadExtensionWithNoLocks(name);
			}
			finally
			{
				_Sync.ExitWriteLock();
			}
		}

		private void UnloadExtensionWithNoLocks(ExtensionName name)
		{
			ExtensionToken token;
			if (!_Tokens.TryGetValue(name, out token))
			{
				return;
			}
			Extension extension;
			ExtensionMetadata metadata = null;
			if (_Extensions.TryGetValue(name, out extension))
			{
				metadata = extension.Metadata;
				_Extensions.Remove(name);
			}

			if (!_Loader.CanUnload)
			{
				return;
			}
			_Loader.Unload(token);
			OnExtensionUnloaded(name, metadata);
		}

		virtual protected void OnExtensionUnloaded(ExtensionName extension, ExtensionMetadata metadata)
		{
			if (ExtensionUnloaded != null)
			{
				ThreadPool.QueueUserWorkItem(
					delegate
						{
							ExtensionUnloaded(
								this, new ExtensionEventArgs(extension, metadata));
						});
			}
		}

		public void EndUnloadExtension(IAsyncResult asyncResult)
		{
			var voidAsyncResult = (VoidAsyncResult)asyncResult;
			voidAsyncResult.EndInvoke();
		}

		public IAsyncResult BeginNotifyAll<TEvent>(TEvent args, AsyncCallback callback, Object state)
			where TEvent : class
		{
			_Sync.EnterReadLock();
			try
			{
				var listeners = FindListeners<TEvent>();

				var asyncResult = new VoidAsyncResult(callback, state, _MessageAccessor);
				ThreadPool.QueueUserWorkItem(
					delegate
					{
						try
						{
							var waitHandles = new WaitHandle[listeners.Count];
							for (var i = 0; i < listeners.Count; i++)
							{
								var extensionName = listeners[i];
								waitHandles[i] = BeginNotify(extensionName, args, null, null).AsyncWaitHandle;
							}
							if (waitHandles.Length > 0)
							{
								WaitHandle.WaitAll(waitHandles);
							}
							asyncResult.SetAsCompleted(null, false);
						}
						catch (Exception e)
						{
							asyncResult.SetAsCompleted(e, false);
						}
					});
				return asyncResult;
			}
			finally
			{
				_Sync.ExitReadLock();
			}
		}

		private IList<ExtensionName> FindListeners<TEvent>()
			where TEvent : class
		{
			var listeners = new List<ExtensionName>();
			foreach(var extension in _Extensions)
			{
				if (!(extension.Value.Instance is IEventListener<TEvent>))
				{
					continue;
				}
				listeners.Add(extension.Key);
			}
			return listeners;
		}

		public void EndNotifyAll(IAsyncResult asyncResult)
		{
			var voidAsyncResult = (VoidAsyncResult)asyncResult;
			voidAsyncResult.EndInvoke();
		}

		public IAsyncResult BeginNotify<TEvent>(ExtensionName extensionName, TEvent args, AsyncCallback callback, Object state)
			where TEvent : class
		{
			_Sync.EnterReadLock();
			try
			{
				Extension extension;
				if (!_Extensions.TryGetValue(extensionName, out extension))
				{
					throw new CannotNotifyExtensionException(
						_MessageAccessor.ExtensionWithGivenNameDoesNotExist(extensionName));
				}
				var listener = extension.Instance as IEventListener<TEvent>;
				if (listener == null)
				{
					throw new CannotNotifyExtensionException(
						_MessageAccessor.ExtensionDoesNotImplementInterfaceForEvent(
							extensionName, typeof(TEvent).Name));
				}

				var asyncResult = new VoidAsyncResult(callback, state, _MessageAccessor);
				ThreadPool.QueueUserWorkItem(
					delegate
						{
							try
							{
								listener.OnEvent(args);
								asyncResult.SetAsCompleted(null, false);
							}
							catch (Exception e)
							{
								asyncResult.SetAsCompleted(e, false);
							}
						});
				return asyncResult;
			}
			finally
			{
				_Sync.ExitReadLock();
			}
		}

		public void EndNotify(IAsyncResult asyncResult)
		{
			var voidAsyncResult = (VoidAsyncResult) asyncResult;
			voidAsyncResult.EndInvoke();
		}

		public IAsyncResult BeginRequest<TRequest, TResponse>(
			ExtensionName extensionName,
			TRequest request,
			AsyncCallback callback,
			Object state)
			where TRequest : class
			where TResponse : class
		{
			_Sync.EnterReadLock();
			try
			{
				Extension extension;
				if (!_Extensions.TryGetValue(extensionName, out extension))
				{
					throw new InvalidRequestException(
						_MessageAccessor.ExtensionWithGivenNameDoesNotExist(extensionName));
				}
				var listener = extension.Instance as IRequestListener<TRequest, TResponse>;
				if (listener == null)
				{
					throw new InvalidRequestException(
						_MessageAccessor.ExtensionDoesNotImplementInterfaceForRequestListener(
							extensionName, typeof(TRequest).Name, typeof(TResponse).Name));
				}
				var asyncResult = new GenericAsyncResult<TResponse>(callback, state, _MessageAccessor);
				ThreadPool.QueueUserWorkItem(
					delegate
					{
						try
						{
							var result = listener.OnRequest(request);
							asyncResult.SetAsCompleted(result, false);
						}
						catch (Exception e)
						{
							asyncResult.SetAsCompleted(e, false);
						}
					});
				return asyncResult;
			}
			finally
			{
				_Sync.ExitReadLock();
			}
		}

		public TResponse EndRequest<TResponse>(IAsyncResult asyncResult)
		{
			var responseAsyncResult = (GenericAsyncResult<TResponse>) asyncResult;
			return responseAsyncResult.EndInvoke();
		}

		internal IList<Object> GetEventListeners(TypeName eventTypeName)
		{
			return FindExtensionsByMetadata(
				metadata => metadata.CanListenToEvent(eventTypeName));
		}

		private IList<Object> FindExtensionsByMetadata(ExtensionMetadataCriteria criteria)
		{
			if (criteria == null)
			{
				throw new ArgumentNullException("criteria");
			}
			var extensions = new List<Object>();
			_Sync.EnterReadLock();
			try
			{
				foreach (var pair in _Extensions)
				{
					var extension = pair.Value;
					var metadata = extension.Metadata;
					if (criteria(metadata))
					{
						extensions.Add(extension.Instance);
					}
				}
			}
			finally
			{
				_Sync.ExitReadLock();
			}
			return extensions;
		}

		private delegate Boolean ExtensionMetadataCriteria(ExtensionMetadata metadata);

		internal IList<Object> GetRequestListeners(TypeName requestTypeName, TypeName responseTypeName)
		{
			return FindExtensionsByMetadata(
				metadata => metadata.CanListenToRequest(requestTypeName, responseTypeName));
		}

		public void Dispose()
		{
			Dispose(false);
			GC.SuppressFinalize(this);
		}

		~Host()
		{
			Dispose(true);
		}

		private void Dispose(Boolean finalizing)
		{
			if (_Disposed)
			{
				return;
			}
			if (!finalizing)
			{
				_Sync.EnterWriteLock();
				try
				{
					if (_Disposed)
					{
						return;
					}
					RemoveAllExtensionsWithNoLocks();
				}
				finally
				{
					_Sync.ExitWriteLock();
				}
				_Sync.Dispose();
			}
			_Disposed = true;
		}
	}
}
